package org.ovirt.engine.core.utils.collections;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A map decorator for providing copies of keys and values upon invocation of accessor methods. With this decorator the
 * caller will be able to safely update the retrieved objects without worrying that the objects held by the map are
 * altered (as he will get copies and not references to the objects held by the map).
 */
public class CopyOnAccessMap<K, V> implements Map<K, V> {

    private static final int REALLOCATION_FACTOR = 2;
    private Map<K, V> innerMap;

    public CopyOnAccessMap(Map<K, V> innerMap) {
        this.innerMap = innerMap;
    }

    @Override
    public int size() {
        return innerMap.size();
    }

    @Override
    public boolean isEmpty() {
        return innerMap.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        return innerMap.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return innerMap.containsValue(value);
    }

    @Override
    public V get(Object key) {
        return clone(innerMap.get(key));
    }

    @SuppressWarnings("unchecked")
    private <O> O clone(O originalKey) {
        // We use an intermediate buffer to hold the serialized form of the
        // object:
        byte[] buffer = null;

        // Serialize the object to an array of bytes:
        ByteArrayOutputStream bufferOut = null;
        ObjectOutputStream objectOut = null;
        try {
            bufferOut = new ByteArrayOutputStream(512);
            objectOut = new ObjectOutputStream(bufferOut);
            objectOut.writeObject(originalKey);
            buffer = bufferOut.toByteArray();
        }
        catch (IOException exception) {
            throw new RuntimeException(exception);
        }
        finally {
            if (objectOut != null) {
                try {
                    objectOut.close();
                }
                catch (IOException exception) {
                    // Ignored.
                }
            }
        }

        // Create a new instance of the object deserializing it from the
        // buffer:
        ByteArrayInputStream bufferIn = null;
        ObjectInputStream objectIn = null;
        try {
            bufferIn = new ByteArrayInputStream(buffer);
            objectIn = new ObjectInputStream(bufferIn) {
                @Override
                protected Class<?> resolveClass(ObjectStreamClass description) throws IOException, ClassNotFoundException {
                    // First try with the context class loader, if that fails
                    // then just call the overridden method:
                    try {
                        return Class.forName(description.getName(), false, Thread.currentThread().getContextClassLoader());
                    }
                    catch (ClassNotFoundException exception) {
                        return super.resolveClass(description);
                    }
                }
            };
            return (O) objectIn.readObject();
        }
        catch (IOException | ClassNotFoundException exception) {
            throw new RuntimeException(exception);
        } finally {
            if (objectIn != null) {
                try {
                    objectIn.close();
                }
                catch (IOException exception) {
                    // Ignored.
                }
            }
        }
    }

    @Override
    public V put(K key, V value) {
        //The old value is no longer in the map, so no need to protect it from external modifiers, so it is not cloned
        return innerMap.put(clone(key), clone(value));
    }

    @Override
    public V remove(Object key) {
        return innerMap.remove(key);
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        for (Entry<? extends K, ? extends V> entry : m.entrySet()) {
            put(entry.getKey(), entry.getValue());
        }
    }

    @Override
    public void clear() {
        innerMap.clear();
    }

    @Override
    public Set<K> keySet() {
        Set<K> newSet = new HashSet<>(innerMap.size() * REALLOCATION_FACTOR);
        for (K key : innerMap.keySet()) {
            newSet.add(clone(key));
        }
        return newSet;
    }

    @Override
    public Collection<V> values() {
        List<V> newValues = new ArrayList<>(innerMap.size() * REALLOCATION_FACTOR);
        for (V value : innerMap.values()) {
            newValues.add(clone(value));
        }
        return newValues;
    }

    @Override
    public Set<Entry<K, V>> entrySet() {
        Map<K, V> newHashMap = new HashMap<>(innerMap.size() * REALLOCATION_FACTOR);
        for (Entry<K, V> entry : innerMap.entrySet()) {
            newHashMap.put(clone(entry.getKey()), clone(entry.getValue()));
        }
        return newHashMap.entrySet();
    }

}
